﻿using System;
using System.Diagnostics;
using System.Windows.Threading;

namespace KineticScroll
{
	/// <summary>
	/// Contains functionality for scrolling with momentum.
	/// </summary>
	internal sealed class KineticScroller
	{
		#region Events

		/// <summary>
		/// Raised when the value of Position has changed.
		/// </summary>
		public event EventHandler PositionChanged;

		#endregion Events

		#region Variables

		private bool _isUserScrolling;
		private DispatcherTimer _timer;

		private double _refreshInterval;
		private double _deceleration;
		private double _maxVelocity;
		private double _scrollRange;

		private double _position;
		private double _newPosition;

		private double _velocity;

		#endregion Variables

		#region Properties

		/// <summary>
		/// Gets or sets the number of milliseconds between refreshing the scroll position.
		/// </summary>
		/// <value>The refresh interval.</value>
		public double RefreshInterval
		{
			get
			{
				return _refreshInterval;
			}
			set
			{
				_refreshInterval = value;
			}
		}

		/// <summary>
		/// Gets or sets the deceleration (in pixels per millisecond) to apply to scrolling each interval.
		/// </summary>
		/// <value>The deceleration.</value>
		public double Deceleration
		{
			get
			{
				return _deceleration;
			}
			set
			{
				_deceleration = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum scrolling velocity (in pixels per millisecond).
		/// </summary>
		/// <value>The max velocity.</value>
		public double MaxVelocity
		{
			get
			{
				return _maxVelocity;
			}
			set
			{
				_maxVelocity = value;
			}
		}

		/// <summary>
		/// Gets or sets the scroll range, e.g. the height of the page that will be scrolled.
		/// </summary>
		/// <value>The scroll range.</value>
		public double ScrollRange
		{
			get
			{
				return _scrollRange;
			}
			set
			{
				_scrollRange = value;
			}
		}

		/// <summary>
		/// Gets or sets the user's position, e.g. the position of the cursor.
		/// </summary>
		/// <value>The position.</value>
		public double Position
		{
			get
			{
				return _position;
			}
			set
			{
				if (_position != value)
				{
					_position = value;
					OnPositionChanged(EventArgs.Empty);
				}
			}
		}

		private double Velocity
		{
			get
			{
				return _velocity;
			}
			set
			{
				// If the value is more than the max then silently fix it
				if (Math.Abs(value) > _maxVelocity)
				{
					_velocity = (value > 0 ? +1 : -1) * _maxVelocity;
				}
				else
				{
					_velocity = value;
				}
			}
		}

		#endregion Properties

		#region Constructor

		/// <summary>
		/// Initializes a new instance of the <see cref="KineticScroller"/> class.
		/// </summary>
		/// <param name="refreshInterval">The refresh interval.</param>
		/// <param name="deceleration">The deceleration.</param>
		/// <param name="maxVelocity">The max velocity.</param>
		/// <param name="scrollRange">The scroll range.</param>
		public KineticScroller(double refreshInterval, double deceleration, double maxVelocity, double scrollRange)
		{
			_refreshInterval = refreshInterval;
			_deceleration = deceleration;
			_maxVelocity = maxVelocity;
			_scrollRange = scrollRange;

			_timer = new DispatcherTimer();
			_timer.Interval = TimeSpan.FromMilliseconds(_refreshInterval);

			_timer.Tick += new EventHandler(Timer_Tick);
		}

		#endregion Constructor

		#region Public Methods

		public void StartUserScroll(double startPosition)
		{
			// Stop any previous scrolling
			StopScrollAnimation();

			// Set the initial positions to where the user started scrolling
			_position = startPosition;
			_newPosition = startPosition;

			// The user is scrolling
			_isUserScrolling = true;

			// Start the animation timer
			_timer.Start();
		}

		public void StopUserScroll()
		{
			// The user has stopped scrolling
			// We don't want to stop the animation timer though, because we will be decelerating
			_isUserScrolling = false;
		}

		public void UpdateUserScroll(double newPosition)
		{
			// The user has scrolled to this position
			// The actual position will be updated in the timer's Tick event handler
			_newPosition = newPosition;
		}

		public void StopScrollAnimation()
		{
			// Stop the animation timer
			_timer.Stop();
		}

		public void ChangeVelocity(double velocityChange)
		{
			// Stop any previous scrolling
			StopScrollAnimation();

			// If we're at a low velocity or changing direction, just set the velocity to 1
			if (Math.Abs(this.Velocity) < 1 || Math.Sign(this.Velocity) != Math.Sign(velocityChange))
			{
				this.Velocity = Math.Sign(velocityChange);
			}
			else
			{
				this.Velocity *= Math.Abs(velocityChange);
			}

			// And start the animation timer
			_timer.Start();
		}

		#endregion Public Methods

		#region Private Scroll Methods

		/// <summary>
		/// Handles the Tick event of the Timer control.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		private void Timer_Tick(object sender, EventArgs e)
		{
			try
			{
				if (_isUserScrolling)
				{
					// Calculate the velocity from the distance the cursor has travelled in the last interval
					double displacement = _newPosition - _position;
					this.Velocity = displacement / _refreshInterval;

					// The user is scrolling, so just go instantly to their point
					this.Position = _newPosition;
				}
				else
				{
					// Change the last sampled velocity by the deceleration
					double changeVelocity = _deceleration / _refreshInterval / _refreshInterval;

					if (changeVelocity > Math.Abs(_velocity))
					{
						this.Velocity = 0;
						StopScrollAnimation();
					}
					else
					{
						this.Velocity -= Math.Sign(this.Velocity) * changeVelocity;
					}

					// The amount the position has changed is given by velocity x time
					this.Position += _velocity * _refreshInterval;
				}
			}
			catch (Exception ex)
			{
				Trace.WriteLine("Timer_Tick:  " + ex.Message);
			}
		}

		#endregion Private Scroll Methods

		#region Event Methods

		/// <summary>
		/// Raises the <see cref="E:PositionChanged"/> event.
		/// </summary>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		private void OnPositionChanged(EventArgs e)
		{
			EventHandler changed = PositionChanged;
			if (changed != null)
			{
				changed(this, e);
			}
		}

		#endregion Event Methods
	}
}
